library RiseAndFall requires ListT optional UnitDex
   
/*
   
    RiseAndFall v1.05
    by Spellbound
   
    Special thanks to Wareditor for the math and re-organising my code and got rid of a lot of 
    needless lines and methods.
   
    This simple library can be used to simulate an airborne effect by causing units to bob up and
    down. It can also be used to make a unit fall from a certain height or rise in the air. The
    purpose of this library is to be used with AttachObject, but one could always find some other
    use for it.
   
   
    _________________________________________________________________________
    REQUIREMENTS
    ˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉ
   
    Required:
   
        ListT: https://www.hiveworkshop.com/threads/containers-list-t.249011/
   
    Optional
   
        UnitDex: https://www.hiveworkshop.com/threads/system-unitdex-unit-indexer.248209/
        UnitDex will replace hashtables for retrieving airbone instances. Highly recommended so
        as to avoid the hashtable limit.
       
    PS if you want units with non-hover or non-flying movement type to be able to levitate, you
    need to give them and then immediately remove Crow Form from the. Alternatively, just get an
    Autofly library and make your life easier. One is available in the test map. Requires UnitDex.
   
   
    _________________________________________________________________________
    API
    ˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉ
   
    operator:
   
        RiseAndFall[yourUnit] will return the RiseAndFall instance of a unit.
   
    static methods:
   
        RiseAndFall.create(unit u, real currentZ, real endZ, real duration, boolean flag)
            ^ This will cause a unit to changed its height from currentZ to endZ over duration. 
            If the flag is true, the unit will decelerate as they reach destination height. This
            can be useful for units that fly or hover.
           
            This function returns the struct instance if you wish to store it.
           
        RiseAndFall.createAirborne(unit u, real currentZ, real endZ, real duration)
            ^ This will make your unit appear to levitate between currentZ and endZ over duration.
            Set a narrow distance for a more realistic effect.
           
            This function returns the struct instance if you wish to store it.
           
        RiseAndFall.isUnitAirborne(unit u)
            ^ This returns true if a unit is airborne/falling/rising.
   
    non-static methods:
   
        RiseAndFall.end(real endZ, real speed)
            ^ Ends the Rise/Fall/Airborne of a unit. real endZ determines the height at which you
            want the unit to end at and speed determines how fast in gets there. use Warcraft 3
            missile speed for referrence.
            
            Set to the current height of the unit to terminate immediately.
   
*/
globals
    private constant real INTERVAL = .03125
endglobals

private module Init
    private static method onInit takes nothing returns nothing
        static if not LIBRARY_UnitDex then
            set instance_storage = InitHashtable()
        endif
        set list = IntegerList.create()
    endmethod
endmodule

struct RiseAndFall
    private unit u
    private real heightStart
    private real heightEnd
    private real speed
    private real progress
    private boolean useSmoothstep
    private boolean isAirborne
   
    private static IntegerList list
    private static timer clock = CreateTimer()
   
    static if LIBRARY_UnitDex then 
        private static integer array instance
    else 
        private static hashtable instance_storage
    endif 
   
    static method operator [] takes unit u returns thistype
        static if LIBRARY_UnitDex then
            return instance[GetUnitId(u)]
        else
            return LoadInteger(instance_storage, GetHandleId(u), 0)
        endif
    endmethod 
   
    static method isUnitAirborne takes unit u returns boolean
        static if LIBRARY_UnitDex then
            return instance[GetUnitId(u)] != 0
        else
            return LoadInteger(instance_storage, GetHandleId(u), 0) != 0
        endif
    endmethod
   
    method destroy takes nothing returns nothing 
        static if LIBRARY_UnitDex then
            set instance[GetUnitId(u)] = 0
        else
            call FlushChildHashtable(instance_storage, GetHandleId(.u))
        endif
        set .u = null
        call list.removeElem(this)
        if list.size() == 0 then
            call PauseTimer(clock)
        endif
        call .deallocate()
    endmethod 
   
    private static method update takes nothing returns nothing
        local thistype this
        local IntegerListItem node = list.first
        local IntegerListItem nodeNext
        local real s
        local real hs
       
        loop
            exitwhen node == 0
            set nodeNext = node.next
            set this = node.data
           
            set .progress = .progress + .speed 
           
            if .progress >= 1.then
                call SetUnitFlyHeight(.u, .heightEnd, 0.)
                if .isAirborne then
                    set hs = .heightStart
                    set .heightStart = .heightEnd
                    set .heightEnd = hs
                    set .progress = 0.
                else
                    call .destroy()
                endif
            else
               
                set s = .progress 
                if .useSmoothstep then
                    call SetUnitFlyHeight(.u, .heightStart + ( .heightEnd - .heightStart ) * (s * s * s * (s * (s * 6 - 15) + 10)), 0.) //smoothstep
                else
                    call SetUnitFlyHeight(.u, .heightStart + ( .heightEnd - .heightStart ) * (s * s), 0.)
                endif
            endif
       
            set node = nodeNext
        endloop
    endmethod
   
    method end takes real endZ, real spd returns nothing
        local real height = GetUnitFlyHeight(.u)
        if endZ != height then 
            set .heightStart = height
            set .heightEnd = endZ
            set .progress = 0.
            set .isAirborne = false
            set .useSmoothstep = false
            if spd == 0. then // divide by zero prevention
                set spd = 600.
            endif
            set .speed = INTERVAL / ((.heightStart - .heightEnd) / spd)
        else 
            call .destroy()
        endif 
    endmethod 
   
    private static method createEx takes unit u, real currentZ, real endZ, real duration, boolean smooth, boolean airborn returns thistype
        local thistype this = RiseAndFall[u]
        
        if this != 0 then
            call this.end(GetUnitFlyHeight(u), 1.)
        endif
        
        set this = allocate()
        set this.u = u
        set this.heightStart = currentZ
        set this.heightEnd = endZ
        set this.progress = 0.
        set this.speed = INTERVAL/duration
        set this.useSmoothstep = smooth
        set this.isAirborne = airborn
       
        static if LIBRARY_UnitDex then
            set instance[GetUnitId(u)] = this
        else
            call SaveInteger(instance_storage, GetHandleId(u), 0, this)
        endif
       
        call list.push(this)
        if list.size() == 1 then
            call TimerStart(clock, INTERVAL, true, function thistype.update)
        endif
       
        return this
    endmethod
   
    static method create takes unit u, real currentZ, real endZ, real duration, boolean smooth returns thistype
        return createEx(u, currentZ, endZ, duration, smooth, false)
    endmethod
   
    static method createAirborne takes unit u, real currentZ, real endZ, real duration returns thistype
        return createEx(u, currentZ, endZ, duration, true, true)
    endmethod
    
    implement Init
endstruct

endlibrary
